#include "server.h"
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>

struct thread_data
{
	Server *server;
	int socket;
	int work_index;
};

void *startReader(void *threadarg)
{
	thread_data *td = (thread_data *)threadarg;
	
	// salveaza informatiile primite ca parametrii, parametrul va fi refolosit
	int socket = td->socket, work_index = td->work_index;
	Server *s = td->server;
	
	printf("Hello! I am the thread that reads from socket #%d!\n", socket);
	s->monitorSocket(socket);
    
	pthread_mutex_lock(&(s->mutex));
	// opreste threadul echivalent de scriere
	s->local_work[work_index] = false;
	pthread_mutex_unlock(&(s->mutex));
	
	printf("Goodbye! I am the thread that reads from socket #%d!\n", socket);
    pthread_exit(NULL);
}

void *startWriter(void *threadarg)
{
	thread_data *td = (thread_data *)threadarg;
	
	// salveaza informatiile primite ca parametrii, parametrul va fi refolosit
	int socket = td->socket, work_index = td->work_index;
	Server *s = td->server;
	
	printf("Hello! I am the thread that writes to socket #%d!\n", socket);
	
	pthread_mutex_lock(&(s->mutex));
	
	// sari din lista de mesaje la ultimul mesaj ... cele dinainte le-a ratat, s-a logat prea tarziu
	list<char*>::iterator i = s->msg.begin(), temp;
	for(temp = s->msg.begin(); temp!=s->msg.end(); ) {
		temp++;
		if(temp!=s->msg.end()) {
			i++;
		}
	}
	
	// atata timp cat trebuie sa ruleze local sau global
	while(s->local_work[work_index] && s->work)
	{
	    pthread_cond_wait(&(s->semnal), &(s->mutex));
		if(s->local_work[work_index] && s->work) // de if e nevoie pentru ultima afisare
		{
			++i;
			printf("Trimit mesaj la socket #%d, length %d, type %d, message %s.\n", socket,((char*)*i)[0],((char*)*i)[1],((char*)*i)+2);
			Socket::writeData(socket, *i, ((char*)*i)[0]);
		}
		pthread_mutex_unlock(&(s->mutex));
		pthread_mutex_lock(&(s->mutex));
	}
	pthread_mutex_unlock(&(s->mutex));
	
	printf("Goodbye! I am the thread that writes to socket #%d!\n", socket);
	pthread_exit(NULL);
}


void Server::start() {
	printf("serverul e pornit pe portul %d\n", m_portNumber);

	if (m_socket.establish(m_portNumber) < 0) {
		printf("Server::start::ERROR: nu pot creea socket la portul specificat (%d)\n", m_portNumber);
		return;
	} 
	
	int t;
	pthread_attr_t attr;

	// ma asigur ca threadurile is joinable
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
	
	int work_index = -1;
	
	for (;;) 
	{
		// asculta daca un client vrea legatura 
		if ((t = m_socket.getConnection()) < 0) 
		{
			if (errno == EINTR) {
				// EINTR ar putea aparea la accept(), incearca din nou 
				continue;
			}
			printf("Server::start::ERROR: a aparut o eroare la initializarea unei conexiuni\n");
			return ; 
		}
		work_index ++;
		printf("Server::start: am acceptat o conexiune de la socketul %d.\n", t);
		thread_data td;
		td.server = this;
		td.socket = t;
		td.work_index = work_index;
		
		// seteaza ca threadurile sa ruleze
		local_work[work_index] = true;
		
		Threads th;

		// creeaza threadul writer
		if (pthread_create(&(th.writer), &attr, startWriter, (void *)&td))
		{
			close(t);
			printf("Server::start::ERROR: la pthread_create()\n");
			exit(-1);
		}
		
		// creeaza threadul reader
		if (pthread_create(&(th.reader), &attr, startReader, (void *)&td))
		{
			close(t);
			printf("Server::start::ERROR: la pthread_create()\n");
			exit(-1);
		}
		
		// salveaza perechea <socket;threaduri>
		ht.insert(std::make_pair(t,th));
	} 
	
	// curatenie, astept sa se gate cititorii, trimit mesaj la scriitori ca e gata
	// treaba si inchid socketu
	for(map<int,Threads>::iterator ii=ht.begin(); ii!=ht.end(); ++ii)
	{
		pthread_join(ii->second.reader,NULL);
	}
	pthread_mutex_lock(&mutex);
	work = 0;
	pthread_cond_broadcast(&semnal);
	pthread_mutex_unlock(&mutex);
	for(map<int,Threads>::iterator ii=ht.begin(); ii!=ht.end(); ++ii)
	{
		pthread_join(ii->second.writer,NULL);
		close(ii->first);
	}
	
	pthread_attr_destroy(&attr);
}

void Server::stop() {
	// Force the server to stop
	
	char mesaj[2] = { 2 , NC_SHUTDOWN };
	sendBroadcast(mesaj);
	
	// trimite la toata lumea mesaje de shutdown
	usleep(500);
	
	// abia acum omoara threadurile
	pthread_mutex_lock(&this->mutex);
	work = 0;
	pthread_mutex_unlock(&this->mutex);
}

int Server::validateUser(const char *username, const char *password) {
	printf("VALIDEZ %s - %s\n", username, password);
	FILE *f = fopen(m_usersFileName, "r");
	if(!f) {
		// daca nu poate fi deschis fisierul cu useri, considera
		// ca nu s-a autentificat corect
		return 0;
	}
	
	char buffer[1000], cur_username[NC_MAX_USERNAME], cur_password[NC_MAX_PASSWORD];
	
	while(1) {
		if(!fgets(buffer, 999, f)) {
			break;
		}
		if(buffer[strlen(buffer)-1] == '\n') {
			buffer[strlen(buffer)-1] = '\0';
		}
		// verifica linie cu linie daca exista userul si parola
		if(sscanf(buffer,"%s | %s",cur_username,cur_password) == 2) {
			if(!strcmp(username, cur_username) && !strcmp(password, cur_password)) {
				fclose(f);
				return 1;
			}
		}
	}
	
	fclose(f);
	return 0;
}

void Server::sendBroadcast(char *message) {
	// executa atomic
	pthread_mutex_lock(&this->mutex);
		char *mesaj = strdup(message);
		this->msg.push_back(mesaj);
	    
	    // anunt pe toti ca am adaugat mesaj la coada
		pthread_cond_broadcast(&this->semnal);
	pthread_mutex_unlock(&this->mutex);
}

void Server::monitorSocket(int clientSocket) {
	int type, terminate = 0, auth_ok = 0;
	char current_message[256], username[256], temp_buffer[256];

	// trimite un username request clientului
	Socket::sendMessage(clientSocket, NC_USERNAME_REQUEST, NULL, true);

	while(terminate == 0) {
		
		// citeste urmatorul mesaj de la client
		Socket::getMessage(clientSocket, type, current_message, true);
		
		switch(type) {
			case NC_USERNAME_RESPONSE: // username response
				// salveaza username
				strcpy(username, current_message);
				
				// trimite un password request
				Socket::sendMessage(clientSocket, NC_PASSWORD_REQUEST, NULL, true);
				break;

			case NC_PASSWORD_RESPONSE: // password response
				// valideaza userul
				if(validateUser(username, current_message)) {
					// userul este valid
					auth_ok = 1;
					Socket::sendMessage(clientSocket, NC_AUTH_OK, NULL, true);
					
					temp_buffer[1] = NC_BROADCAST;
					strcpy(temp_buffer+2, "*");
					strcat(temp_buffer+2, username);
					strcat(temp_buffer+2, " tocmai s-a logat");
					temp_buffer[0] = strlen(temp_buffer+2) + 2;
					
					sendBroadcast(temp_buffer);
				} else {
					// userul este invalid
					auth_ok = 0;
					Socket::sendMessage(clientSocket, NC_AUTH_FAILED, NULL, true);
					
					// trimite din nou un username request
					Socket::sendMessage(clientSocket, NC_USERNAME_REQUEST, NULL, true);
				}				
				break;

			case NC_MESSAGE: // broadcast message
				if(auth_ok) {
					// userul este validat, poate trimite mesaje
					temp_buffer[1] = NC_BROADCAST;
					strcpy(temp_buffer+2, username);
					strcat(temp_buffer+2, ": ");
					strcat(temp_buffer+2, current_message);
					temp_buffer[0] = strlen(temp_buffer+2) + 2;
					
					// trimite temp_buffer la toata lumea
					sendBroadcast(temp_buffer);
				} else {
					// userul nu este validat, trimite din nou un username request
					Socket::sendMessage(clientSocket, NC_USERNAME_REQUEST, NULL, true);
				}
				break;

			case NC_LOGOUT: // logout message
				// confirma printr-un shutdown message
				Socket::sendMessage(clientSocket, NC_SHUTDOWN, NULL, true);
				
				temp_buffer[1] = NC_BROADCAST;
				strcpy(temp_buffer+2, "*");
				strcat(temp_buffer+2, username);
				strcat(temp_buffer+2, " tocmai a iesit");
				temp_buffer[0] = strlen(temp_buffer+2) + 2;
				
				sendBroadcast(temp_buffer);
				
				terminate = 1;
				break;
		}
	}
}

Server::Server(int portNumber, const char *usersFileName) {
	m_portNumber = portNumber;
	m_usersFileName = new char[strlen(usersFileName)+1];
	strcpy(m_usersFileName, usersFileName);
	work = 1;
	//initializare concurenta
	pthread_mutex_init(&mutex, NULL);
	pthread_cond_init (&semnal, NULL);
}

Server::~Server() {
	if(m_usersFileName) {
		delete []m_usersFileName;
		m_usersFileName = 0;
	}
	pthread_mutex_destroy(&mutex);
	pthread_cond_destroy(&semnal);
}
